﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Opennav.MapGenerator.Geometry;

namespace Opennav.MapGenerator.Locator
{
    public class LocatorEngine
    {
        private List<GeoPlace> _places = new List<GeoPlace>();
        private List<GeoArea> _root = new List<GeoArea>();
        private List<GeoArea> _areas = new List<GeoArea>();

        public IEnumerable<GeoPlace> Places { get { return _places; } }

        public void AddArea(string name, int level, Multipolygon polygon) 
        {
            GeoArea area = new GeoArea(name, level, polygon);
            _areas.Add(area);
            foreach (GeoArea rootArea in _root)
            {
                if (ThrowIn(area, rootArea))
                    return;
            }
            List<GeoArea> fits = new List<GeoArea>();
            foreach (GeoArea rootArea in _root)
            {
                if (ThrowIn(rootArea, area))
                    fits.Add(rootArea);
            }
            foreach (GeoArea remove in fits)
            {
                _root.Remove(remove);
            }
            _root.Add(area);
        }

        public GeoPlace AddPlace(string name, string isin, int level, Coordinate coordinate)
        {
            GeoPlace place = new GeoPlace(name, isin, level, coordinate);
            foreach (GeoArea rootArea in _root)
            {
                if (ThrowIn(place, rootArea))
                {
                    _places.Add(place);
                    return place;
                }
            }
            return null;
            // Skipped
        }

        public void Process()
        {
            foreach (GeoPlace place in _places)
            {
                GeoPlace nearest = FindNearest(place.Coordinate, place.ParentArea, place.Level - 1);
                if (nearest != null) place.Parent = nearest;
            }
        }

        public GeoPlace FindPlace(Coordinate coordinate)
        {
            GeoArea locArea = FindArea(coordinate);
            if (locArea == null)
                return null;
            return FindNearest(coordinate, locArea, int.MaxValue);
        }

        private GeoPlace FindNearest(Coordinate coordinate, GeoArea area, int maxLevel)
        {
            GeoPlace nearest = null;
            int minDistance = int.MaxValue;
            foreach (GeoPlace place in area.Places)
            {
                if (place.Level <= maxLevel)
                {
                    int distance = (int)place.Coordinate.Distance(coordinate);
                    if (distance < minDistance)
                    {
                        nearest = place;
                        minDistance = distance;
                    }
                }
            }
            if (nearest != null)
                return nearest;
            if (area.Parent == null)
                return null;
            return FindNearest(coordinate, area.Parent, maxLevel);
        }

        private GeoArea FindArea(Coordinate coordinate, GeoArea area)
        {
            if (!area.Polygon.Contains(coordinate))
                return null;
            foreach (GeoArea subarea in area.Subareas) 
            {
                GeoArea lowerArea = FindArea(coordinate, subarea);
                if (lowerArea != null)
                    return lowerArea;
            }
            return area;
        }

        public GeoArea FindArea(Coordinate coordinate)
        {
            foreach (GeoArea rootArea in _root) 
            {
                GeoArea result = FindArea(coordinate, rootArea);
                if (result != null)
                    return result;
            }
            return null;
        }

        private bool ThrowIn(GeoArea area, GeoArea into)
        {
            if (area.Level <= into.Level)
                return false;
            if (!into.Polygon.Contains(area.Polygon))
                return false;
            foreach (GeoArea childArea in into.Subareas)
            {
                if (ThrowIn(area, childArea))
                    return true;
            }
            List<GeoArea> fits = new List<GeoArea>();
            foreach (GeoArea childArea in into.Subareas)
            {
                if (ThrowIn(childArea, area))
                    fits.Add(childArea);
            }
            foreach (GeoArea remove in fits)
            {
                into.Subareas.Remove(remove);
            }
            area.Parent = into;
            into.Subareas.Add(area);
            return true;
        }

        private bool ThrowIn(GeoPlace place, GeoArea into)
        {
            if (place.Level < into.Level)
                return false;
            if (!into.Polygon.Contains(place.Coordinate))
                return false;
            foreach (GeoArea subarea in into.Subareas)
            {
                if (ThrowIn(place, subarea))
                    return true;
            }
            into.Places.Add(place);
            place.ParentArea = into;
            return true;
        }
    }
}
